JS类型转换

前面讲了基本类型,这篇来总结下类型转换。

因为JS是弱类型语言,所以类型转换发生非常频繁,大部分我们熟悉的运算都会先进行类型转换。大部分类型转换符合人类的直觉,但是如果我们不去理解类型转换的严格定义,很容易造成一些代码中的判断失误。

其中最为臭名昭著的是JS中的“ == ”运算,因为试图实现跨类型的比较,它的规则复杂到几乎没人可以记住。这里也不打算讲解==的规则,它属于设计失误,并非语言中有价值的部分,很多实践中推荐禁止使用“ ==”,而要求程序员进行显式地类型转换后,用 === 比较。

其它运算,如加减乘除大于小于,也都会涉及类型转换。幸好的是,实际上大部分类型转换规则是非常简单的,如下表所示:

在这个里面,较为复杂的部分是Number和String之间的转换,以及对象跟基本类型之间的转换。

StringToNumber

字符串到数字的类型转换,存在一个语法结构,类型转换支持十进制、二进制、八进制和十六进制,比如:

  • 30;
  • 0b111;
  • 0o13;
  • 0xFF。

此外,JavaScript支持的字符串语法还包括正负号科学计数法,可以使用大写或者小写的e来表示:

  • 1e3;
  • -1e-2。

需要注意的是,parseInt 和 parseFloat 并不使用这个转换,所以支持的语法跟这里不尽相同。在不传入第二个参数的情况下,parseInt只支持16进制前缀“0x”,而且会忽略非数字字符,也不支持科学计数法。

在一些古老的浏览器环境中,parseInt还支持0开头的数字作为8进制前缀,这是很多错误的来源。所以在任何环境下,都建议传入parseInt的第二个参数,而parseFloat则直接把原字符串作为十进制来解析,它不会引入任何的其他进制。

来看2个例题:

1
2
3
4
5
var arr = ["", '1', '2', '3'];
var r = arr.map(parseInt);

var arr2=['1','4','9','16'];
var r2=arr2.map(parseInt)

这两个例题的类型是一致的,只是数组不同。去试试输出结果是什么?

跟你想象的是否一致?因为传给map的参数有三个:当前的值、当前值的索引、以及当前数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
当前的值(currentValuve)//依次是'1','4','9','16'

当前值的索引(currentIndex)//依次是0,1,2,3

以及当前数组(currentArray)//['1','4','9','16'],每次都是这个

而每次使用parseInt()函数的时候,却只传入了两个值(currentValue,currentIndex)

所以结果就是:

parseInt('1',0)//1 (第二个参数假如经过 Number 函数转换后为 0 或 NaN,则将会忽略——Mozilla官方文档)
parseInt('4',1)//非法,NaN

parseInt('9',2)//非法,NaN

parseInt('16',3)//这里值得注意,在解析字符串'16'的时候,发现6大于或等于3,所以其后的数字都被忽略,只剩下一个1返回了(如果 parseInt 遇到了不属于radix参数所指定的基数中的字符那么该字符和其后的字符都将被忽略。——Mozilla官方文档)

最后,关于 ["", '1', '2', '3'] 输出为 NaN,NaN,NaNNaN 是因为第一个为空,转换为NaN,后面的为parseInt(1,1),parseInt(2,2),parseInt(3,3)为NaN则是因为数值不能大于进制基数,例如2进制的最大值为1,10进制的最大值为9

多数情况下,Number 是比 parseInt 和 parseFloat 更好的选择。

如果不使用 Number、parseInt、parseFloat怎么把字符串转换为number呢?

这就是js中把字符类型转成数字类型第三种方法:利用js变量弱类型转换。

1
2
3
4
var num = +"1000"
"1000">>>0
~~"1000"
"1000"*1

详细步骤可看:这里

第4种方法则是利用数学函数

1
2
3
Math.floor("1000")
Math.round("1000")
Math.ceil("1000")

NumberToString

在较小的范围内,数字到字符串的转换是完全符合我们直觉的十进制表示。当Number绝对值较大或者较小时,字符串表示则是使用科学计数法表示的。这个算法细节繁多,其实就是保证了产生的字符串不会过长。具体的算法,可以去参考JavaScript的语言标准。因为在日常开发中很少用到,所以这里就不去详细地讲解了。

装箱

每一种基本类型Number、String、Boolean、Symbol在对象中都有对应的类,所谓装箱转换,正是把基本类型转换为对应的对象,它是类型转换中一种相当重要的种类。

前面说过,全局的 Symbol 函数无法使用 new 来调用,但我们仍可以利用装箱机制来得到一个 Symbol 对象,我们可以利用一个函数的call方法来强迫产生装箱。我们定义一个函数,函数里面只有return this,然后我们调用函数的call方法到一个Symbol类型的值上,这样就会产生一个symbolObject。我们可以用console.log看一下这个东西的type of,它的值是object,我们使用symbolObject instanceof 可以看到,它是Symbol这个类的实例,我们找它的constructor也是等于Symbol的,所以我们无论从哪个角度看,它都是Symbol装箱过的对象:

1
2
3
4
5
var symbolObject = (function(){ return this; }).call(Symbol("a"));

console.log(typeof symbolObject); //object
console.log(symbolObject instanceof Symbol); //true
console.log(symbolObject.constructor == Symbol); //true

装箱机制会频繁产生临时对象,在一些对性能要求较高的场景下,我们应该尽量避免对基本类型做装箱转换。使用内置的 Object 函数,我们可以在JavaScript代码中显式调用装箱能力。

1
2
3
4
5
var symbolObject = Object(Symbol("a"));

console.log(typeof symbolObject); //object
console.log(symbolObject instanceof Symbol); //true
console.log(symbolObject.constructor == Symbol); //true

每一类装箱对象皆有私有的 Class 属性,这些属性可以用 Object.prototype.toString 获取:

1
2
3
var symbolObject = Object(Symbol("a"));

console.log(Object.prototype.toString.call(symbolObject)); //[object Symbol]

在 JavaScript 中,没有任何方法可以更改私有的 Class 属性,因此Object.prototype.toString 是可以准确识别对象对应的基本类型的方法,它比 instanceof 更加准确。

但需要注意的是,call本身会产生装箱操作,所以需要配合 typeof 来区分基本类型还是对象类型。

拆箱

在JavaScript标准中,规定了 ToPrimitive 函数,它是对象类型到基本类型的转换(即,拆箱转换)。

对象到 String 和 Number 的转换都遵循“先拆箱再转换”的规则。通过拆箱转换,把对象变成基本类型,再从基本类型转换为对应的 String 或者 Number。

拆箱转换会尝试调用 valueOf 和 toString 来获得拆箱后的基本类型。如果 valueOf 和 toString 都不存在,或者没有返回基本类型,则会产生类型错误 TypeError。

1
2
3
4
5
6
7
8
var o = {
valueOf : () => {console.log("valueOf"); return {}},
toString : () => {console.log("toString"); return {}}
}
o * 2
// valueOf
// toString
// TypeError

我们定义了一个对象o,o有valueOf和toString两个方法,这两个方法都返回一个对象,然后我们进行o*2这个运算的时候,你会看见先执行了valueOf,接下来是toString,最后抛出了一个TypeError,这就说明了这个拆箱转换失败了。

到 String 的拆箱转换会优先调用 toString。我们把刚才的运算从o*2换成 String(o),那么你会看到调用顺序就变了。

1
2
3
4
5
6
7
8
9
 var o = {
valueOf : () => {console.log("valueOf"); return {}},
toString : () => {console.log("toString"); return {}}
}

String(o)
// toString
// valueOf
// TypeError

不过如果把 String(0)转为o+"2"则执行顺序将会变为

1
2
3
4
5
6
7
8
9
var o = {
valueOf : () => {console.log("valueOf"); return {}},
toString : () => {console.log("toString"); return {}}
}

o+"2"
// valueOf
// toString
// TypeError

在 ES6 之后,还允许对象通过显式指定 @@toPrimitive Symbol 来覆盖原有的行为。

1
2
3
4
5
6
7
8
9
10
11
var o = {
valueOf : () => {console.log("valueOf"); return {}},
toString : () => {console.log("toString"); return {}}
}

o[Symbol.toPrimitive] = () => {console.log("toPrimitive"); return "hello"}


console.log(o + "")
// toPrimitive
// hello

总结

有一个说法是:程序 = 算法 + 数据结构,运行时类型包含了所有 JavaScript 执行时所需要的数据结构的定义,所以要对它格外重视。